/**
 * User: yuhao
 * Date: 14-12-17
 * Time: 下午3:12
 * HeatmapFactory的封装
 */

define("require".split(" "), function () {

        return (function () {
        // store object constructor
        // a heatmap contains a store
        // the store has to know about the heatmap in order to trigger heatmap updates when datapoints get added
        var store = function store(hmap) {
            var _ = {
                // data is a two dimensional array
                // a datapoint gets saved as data[point-x-value][point-y-value]
                // the value at [point-x-value][point-y-value] is the occurrence of the datapoint
                data:[],
                // tight coupling of the heatmap object
                heatmap:hmap
            };
            // the max occurrence - the heatmaps radial gradient alpha transition is based on it
            this.max = 1;
            this.get = function (key) {
                return _[key];
            };
            this.set = function (key, value) {
                _[key] = value;
            };
        };

        store.prototype = {
            // function for adding datapoints to the store
            // datapoints are usually defined by x and y but could also contain a third parameter which represents the occurrence
            addDataPoint:function (x, y) {
                if (x < 0 || y < 0) {
                    return;
                }
                var me = this,
                    heatmap = me.get("heatmap"),
                    data = me.get("data");

                if (!data[x]) {
                    data[x] = [];
                }
                if (!data[x][y]) {
                    data[x][y] = 0;
                }
                // if count parameter is set increment by count otherwise by 1
                data[x][y] += (arguments.length < 3) ? 1 : arguments[2];
                me.set("data", data);
                // do we have a new maximum?
                if (me.max < data[x][y]) {
                    // max changed, we need to redraw all existing(lower) datapoints
                    heatmap.get("actx").clearRect(0, 0, heatmap.get("width"), heatmap.get("height"));
                    me.setDataSet({ max:data[x][y], data:data }, true);
                    return;
                }
                heatmap.drawAlpha(x, y, data[x][y], true);
            },

            setDataSet:function (obj, internal) {
                var me = this,
                    heatmap = me.get("heatmap"),
                    data = [],
                    d = obj.data,
                    dlen = d.length;

                // clear the heatmap before the data set gets drawn
                heatmap.clear();
                this.max = obj.max;
                // if a legend is set, update it
                heatmap.get("legend") && heatmap.get("legend").update(obj.max);

                if (internal != null && internal) {
                    for (var one in d) {
                        // jump over undefined indexes
                        if (one === undefined) {
                            continue;
                        }
                        for (var two in d[one]) {
                            if (two === undefined) {
                                continue;
                            }
                            // if both indexes are defined, push the values into the array
                            heatmap.drawAlpha(one, two, d[one][two], false);
                        }
                    }
                } else {
                    while (dlen--) {
                        var point = d[dlen];
                        heatmap.drawAlpha(point.x, point.y, point.count, false);
                        if (!data[point.x]) {
                            data[point.x] = [];
                        }
                        if (!data[point.x][point.y]) {
                            data[point.x][point.y] = 0;
                        }
                        data[point.x][point.y] = point.count;
                    }
                }
                heatmap.colorize();
                this.set("data", d);
            },

            exportDataSet:function () {
                var me = this,
                    data = me.get("data"),
                    exportData = [];

                for (var one in data) {
                    // jump over undefined indexes
                    if (one === undefined) {
                        continue;
                    }
                    for (var two in data[one]) {
                        if (two === undefined) {
                            continue;
                        }
                        // if both indexes are defined, push the values into the array
                        exportData.push({ x:parseInt(one, 10), y:parseInt(two, 10), count:data[one][two] });
                    }
                }
                return { max:me.max, data:exportData };
            },

            generateRandomDataSet:function (points) {
                var heatmap = this.get("heatmap"),
                    w = heatmap.get("width"),
                    h = heatmap.get("height"),
                    randomset = {},
                    max = Math.floor(Math.random() * 1000 + 1);

                randomset.max = max;
                var data = [];
                while (points--) {
                    data.push({
                        x:Math.floor(Math.random() * w + 1),
                        y:Math.floor(Math.random() * h + 1),
                        count:Math.floor(Math.random() * max + 1)
                    });
                }
                randomset.data = data;
                this.setDataSet(randomset);
            }
        };

        var legend = function legend(config) {
            this.config = config;
            var _ = {
                element:null,
                labelsEl:null,
                gradientCfg:null,
                ctx:null
            };
            this.get = function (key) {
                return _[key];
            };
            this.set = function (key, value) {
                _[key] = value;
            };
            this.init();
        };

        legend.prototype = {
            init:function () {
                var me = this,
                    config = me.config,
                    title = config.title || "Legend",
                    position = config.position,
                    offset = config.offset || 10,
                    gconfig = config.gradient,
                    labelsEl = document.createElement("ul"),
                    labelsHtml = "",
                    positionCss = "",
                    grad, element, gradient;

                me.processGradientObject();
                // Positioning
                // top or bottom
                if (position.indexOf('t') > -1) {
                    positionCss += 'top:' + offset + 'px;';
                } else {
                    positionCss += 'bottom:' + offset + 'px;';
                }
                // left or right
                if (position.indexOf('l') > -1) {
                    positionCss += 'left:' + offset + 'px;';
                } else {
                    positionCss += 'right:' + offset + 'px;';
                }
                element = document.createElement("div");
                element.style.cssText = "border-radius:5px;position:absolute;" + positionCss +
                    "font-family:Helvetica;width:256px;z-index:10000;background:rgba(255,255,255,1);padding:10px;border:1px solid black;margin:0;";
                element.innerHTML = "<h3 style='padding:0;margin:0;text-align:center;font-size:16px;'>" + title + "</h3>";
                // create gradient in canvas
                labelsEl.style.cssText = "position:relative;font-size:12px;display:block;list-style:none;list-style-type:none;margin:0;height:15px;";
                // create gradient element
                gradient = document.createElement("div");
                gradient.style.cssText = ["position:relative;display:block;width:256px;height:15px;border-bottom:1px solid black; background-image:url(", me.createGradientImage(), ");"].join("");
                element.appendChild(labelsEl);
                element.appendChild(gradient);
                me.set("element", element);
                me.set("labelsEl", labelsEl);
                me.update(1);
            },

            processGradientObject:function () {
                // create array and sort it
                var me = this,
                    gradientConfig = this.config.gradient,
                    gradientArr = [];

                for (var key in gradientConfig) {
                    if (gradientConfig.hasOwnProperty(key)) {
                        gradientArr.push({ stop:key, value:gradientConfig[key] });
                    }
                }
                gradientArr.sort(function (a, b) {
                    return (a.stop - b.stop);
                });
                gradientArr.unshift({ stop:0, value:'rgba(0,0,0,0)' });
                me.set("gradientArr", gradientArr);
            },

            createGradientImage:function () {
                var me = this,
                    gradArr = me.get("gradientArr"),
                    length = gradArr.length,
                    canvas = document.createElement("canvas"),
                    ctx = canvas.getContext("2d"),
                    grad;

                // the gradient in the legend including the ticks will be 256x15px
                canvas.width = "256";
                canvas.height = "15";
                grad = ctx.createLinearGradient(0, 5, 256, 10);
                for (var i = 0; i < length; i++) {
                    grad.addColorStop(1 / (length - 1) * i, gradArr[i].value);
                }
                ctx.fillStyle = grad;
                ctx.fillRect(0, 5, 256, 10);
                ctx.strokeStyle = "black";
                ctx.beginPath();
                for (var i = 0; i < length; i++) {
                    ctx.moveTo(((1 / (length - 1) * i * 256) >> 0) + 0.5, 0);
                    ctx.lineTo(((1 / (length - 1) * i * 256) >> 0) + 0.5, (i == 0) ? 15 : 5);
                }
                ctx.moveTo(255.5, 0);
                ctx.lineTo(255.5, 15);
                ctx.moveTo(255.5, 4.5);
                ctx.lineTo(0, 4.5);
                ctx.stroke();
                // we re-use the context for measuring the legends label widths
                me.set("ctx", ctx);
                return canvas.toDataURL();
            },

            getElement:function () {
                return this.get("element");
            },

            update:function (max) {
                var me = this,
                    gradient = me.get("gradientArr"),
                    ctx = me.get("ctx"),
                    labels = me.get("labelsEl"),
                    labelsHtml = "",
                    labelText, offset;

                for (var i = 0; i < gradient.length; i++) {
                    labelText = max * gradient[i].stop >> 0;
                    offset = (ctx.measureText(labelText).width / 2) >> 0;
                    if (i == 0) {
                        offset = 0;
                    }
                    if (i == gradient.length - 1) {
                        offset *= 2;
                    }
                    labelsHtml += '<li style="position:absolute;left:' + (((((1 / (gradient.length - 1) * i * 256) || 0)) >> 0) - offset + 0.5) + 'px">' + labelText + '</li>';
                }
                labels.innerHTML = labelsHtml;
            }
        };

        // heatmap object constructor
        var heatmap = function heatmap(config) {
            // private variables
            var _ = {
                radius:40,
                element:{},
                canvas:{},
                acanvas:{},
                ctx:{},
                actx:{},
                legend:null,
                visible:true,
                width:0,
                height:0,
                max:false,
                gradient:false,
                opacity:180,
                premultiplyAlpha:false,
                bounds:{
                    l:1000,
                    r:0,
                    t:1000,
                    b:0
                },
                debug:false
            };
            // heatmap store containing the datapoints and information about the maximum
            // accessible via instance.store
            this.store = new store(this);
            this.get = function (key) {
                return _[key];
            };
            this.set = function (key, value) {
                _[key] = value;
            };
            // configure the heatmap when an instance gets created
            this.configure(config);
            // and initialize it
            this.init();
        };

        // public functions
        heatmap.prototype = {
            configure:function (config) {
                var me = this,
                    rout, rin;

                me.set("radius", config["radius"] || 40);
                me.set("element", (config.element instanceof Object) ? config.element : document.getElementById(config.element));
                me.set("visible", (config.visible != null) ? config.visible : true);
                me.set("max", config.max || false);
                // default is the common blue to red gradient
                me.set("gradient", config.gradient || { "0.45":"rgb(0,0,255)", "0.55":"rgb(0,255,255)", "0.65":"rgb(0,255,0)", "0.95":"yellow", "1.0":"rgb(255,0,0)" });
                me.set("opacity", parseInt(255 / (100 / config.opacity), 10) || 180);
                me.set("width", config.width || 0);
                me.set("height", config.height || 0);
                me.set("debug", config.debug);

                if (config.legend) {
                    var legendCfg = config.legend;
                    legendCfg.gradient = me.get("gradient");
                    me.set("legend", new legend(legendCfg));
                }
            },

            resize:function () {
                var me = this,
                    element = me.get("element"),
                    canvas = me.get("canvas"),
                    acanvas = me.get("acanvas");

                canvas.width = acanvas.width = me.get("width") || element.style.width.replace(/px/, "") || me.getWidth(element);
                this.set("width", canvas.width);
                canvas.height = acanvas.height = me.get("height") || element.style.height.replace(/px/, "") || me.getHeight(element);
                this.set("height", canvas.height);
            },

            init:function () {
                var me = this,
                    canvas = document.createElement("canvas"),
                    acanvas = document.createElement("canvas"),
                    ctx = canvas.getContext("2d"),
                    actx = acanvas.getContext("2d"),
                    element = me.get("element");

                me.initColorPalette();
                me.set("canvas", canvas);
                me.set("ctx", ctx);
                me.set("acanvas", acanvas);
                me.set("actx", actx);
                me.resize();
                canvas.style.cssText = acanvas.style.cssText = "position:absolute;top:0;left:0;z-index:10000;";

                if (!me.get("visible")) {
                    canvas.style.display = "none";
                }
                element.appendChild(canvas);
                if (me.get("legend")) {
                    element.appendChild(me.get("legend").getElement());
                }
                // debugging purposes only
                if (me.get("debug")) {
                    document.body.appendChild(acanvas);
                }
                actx.shadowOffsetX = 15000;
                actx.shadowOffsetY = 15000;
                actx.shadowBlur = 15;
            },

            initColorPalette:function () {
                var me = this,
                    canvas = document.createElement("canvas"),
                    gradient = me.get("gradient"),
                    ctx, grad, testData;

                canvas.width = "1";
                canvas.height = "256";
                ctx = canvas.getContext("2d");
                grad = ctx.createLinearGradient(0, 0, 1, 256);
                // Test how the browser renders alpha by setting a partially transparent pixel
                // and reading the result. A good browser will return a value reasonably close
                // to what was set. Some browsers (e.g. on Android) will return a ridiculously wrong value.
                testData = ctx.getImageData(0, 0, 1, 1);
                testData.data[0] = testData.data[3] = 64; // 25% red & alpha
                testData.data[1] = testData.data[2] = 0; // 0% blue & green
                ctx.putImageData(testData, 0, 0);
                testData = ctx.getImageData(0, 0, 1, 1);
                me.set("premultiplyAlpha", (testData.data[0] < 60 || testData.data[0] > 70));
                for (var x in gradient) {
                    grad.addColorStop(x, gradient[x]);
                }
                ctx.fillStyle = grad;
                ctx.fillRect(0, 0, 1, 256);
                me.set("gradient", ctx.getImageData(0, 0, 1, 256).data);
            },

            getWidth:function (element) {
                var width = element.offsetWidth;
                if (element.style.paddingLeft) {
                    width += element.style.paddingLeft;
                }
                if (element.style.paddingRight) {
                    width += element.style.paddingRight;
                }
                return width;
            },

            getHeight:function (element) {
                var height = element.offsetHeight;
                if (element.style.paddingTop) {
                    height += element.style.paddingTop;
                }
                if (element.style.paddingBottom) {
                    height += element.style.paddingBottom;
                }
                return height;
            },

            colorize:function (x, y) {
                // get the private variables
                var me = this,
                    width = me.get("width"),
                    radius = me.get("radius"),
                    height = me.get("height"),
                    actx = me.get("actx"),
                    ctx = me.get("ctx"),
                    x2 = radius * 3,
                    premultiplyAlpha = me.get("premultiplyAlpha"),
                    palette = me.get("gradient"),
                    opacity = me.get("opacity"),
                    bounds = me.get("bounds"),
                    left, top, bottom, right, image, imageData, length, alpha, offset, finalAlpha;

                if (x != null && y != null) {
                    if (x + x2 > width) {
                        x = width - x2;
                    }
                    if (x < 0) {
                        x = 0;
                    }
                    if (y < 0) {
                        y = 0;
                    }
                    if (y + x2 > height) {
                        y = height - x2;
                    }
                    left = x;
                    top = y;
                    right = x + x2;
                    bottom = y + x2;
                } else {
                    if (bounds['l'] < 0) {
                        left = 0;
                    } else {
                        left = bounds['l'];
                    }
                    if (bounds['r'] > width) {
                        right = width;
                    } else {
                        right = bounds['r'];
                    }
                    if (bounds['t'] < 0) {
                        top = 0;
                    } else {
                        top = bounds['t'];
                    }
                    if (bounds['b'] > height) {
                        bottom = height;
                    } else {
                        bottom = bounds['b'];
                    }
                }
                image = actx.getImageData(left, top, right - left, bottom - top);
                imageData = image.data;
                length = imageData.length;
                // loop thru the area
                for (var i = 3; i < length; i += 4) {
                    // [0] -> r, [1] -> g, [2] -> b, [3] -> alpha
                    alpha = imageData[i],
                        offset = alpha * 4;
                    if (!offset) {
                        continue;
                    }
                    // we ve started with i = 3
                    // set the new r, g and b values
                    finalAlpha = (alpha < opacity) ? alpha : opacity;
                    imageData[i - 3] = palette[offset];
                    imageData[i - 2] = palette[offset + 1];
                    imageData[i - 1] = palette[offset + 2];

                    if (premultiplyAlpha) {
                        // To fix browsers that premultiply incorrectly, we'll pass in a value scaled
                        // appropriately so when the multiplication happens the correct value will result.
                        imageData[i - 3] /= 255 / finalAlpha;
                        imageData[i - 2] /= 255 / finalAlpha;
                        imageData[i - 1] /= 255 / finalAlpha;
                    }
                    // we want the heatmap to have a gradient from transparent to the colors
                    // as long as alpha is lower than the defined opacity (maximum), we'll use the alpha value
                    imageData[i] = finalAlpha;
                }
                // the rgb data manipulation didn't affect the ImageData object(defined on the top)
                // after the manipulation process we have to set the manipulated data to the ImageData object
                image.data = imageData;
                ctx.putImageData(image, left, top);
            },

            drawAlpha:function (x, y, count, colorize) {
                // storing the variables because they will be often used
                var me = this,
                    radius = me.get("radius"),
                    ctx = me.get("actx"),
                //max = me.get("max"),
                    bounds = me.get("bounds"),
                    xb = x - (1.5 * radius) >> 0,
                    yb = y - (1.5 * radius) >> 0,
                    xc = x + (1.5 * radius) >> 0,
                    yc = y + (1.5 * radius) >> 0;

                ctx.shadowColor = ('rgba(0, 0, 0,' + ((count) ? (count / me.store.max) : '0.1') + ')');
                ctx.shadowOffsetX = 15000;
                ctx.shadowOffsetY = 15000;
                ctx.shadowBlur = 15;
                ctx.beginPath();
                ctx.arc(x - 15000, y - 15000, radius, 0, Math.PI * 2, true);
                ctx.closePath();
                ctx.fill();
                if (colorize) {
                    // finally colorize the area
                    me.colorize(xb, yb);
                } else {
                    // or update the boundaries for the area that then should be colorized
                    if (xb < bounds["l"]) {
                        bounds["l"] = xb;
                    }
                    if (yb < bounds["t"]) {
                        bounds["t"] = yb;
                    }
                    if (xc > bounds['r']) {
                        bounds['r'] = xc;
                    }
                    if (yc > bounds['b']) {
                        bounds['b'] = yc;
                    }
                }
            },

            toggleDisplay:function () {
                var me = this,
                    visible = me.get("visible"),
                    canvas = me.get("canvas");

                if (!visible) {
                    canvas.style.display = "block";
                } else {
                    canvas.style.display = "none";
                }
                me.set("visible", !visible);
            },

            // dataURL export
            getImageData:function () {
                return this.get("canvas").toDataURL();
            },

            clear:function () {
                var me = this,
                    w = me.get("width"),
                    h = me.get("height");

                me.store.set("data", []);
                // @TODO: reset stores max to 1
                //me.store.max = 1;
                me.get("ctx").clearRect(0, 0, w, h);
                me.get("actx").clearRect(0, 0, w, h);
            },

            cleanup:function () {
                var me = this;
                me.get("element").removeChild(me.get("canvas"));
            }
        };

        return {
            create:function (config) {
                return new heatmap(config);
            },
            util:{
                mousePosition:function (ev) {
                    // this doesn't work right
                    // rather use
                    /*
                     // this = element to observe
                     var x = ev.pageX - this.offsetLeft;
                     var y = ev.pageY - this.offsetTop;
                     */
                    var x, y;
                    if (ev.layerX) { // Firefox
                        x = ev.layerX;
                        y = ev.layerY;
                    } else if (ev.offsetX) { // Opera
                        x = ev.offsetX;
                        y = ev.offsetY;
                    }
                    if (typeof x == 'undefined') {
                        return;
                    }
                    return [x, y];
                }
            }
        };
    })();
});